#!/bin/bash
{
	#////////////////////////////////////
	# DietPi Sync
	#
	#////////////////////////////////////
	# Created by Daniel Knight / daniel.knight@dietpi.com / dietpi.com
	#
	#////////////////////////////////////
	#
	# Info:
	# - Location: /boot/dietpi/dietpi-sync
	# - Allows user to sync a Source and Target directory.
	#
	# Usage:
	# - /boot/dietpi/dietpi-sync		= Menu
	# - /boot/dietpi/dietpi-sync 1		= Sync
	#////////////////////////////////////

	# Import DietPi-Globals --------------------------------------------------------------
	. /boot/dietpi/func/dietpi-globals
	readonly G_PROGRAM_NAME='DietPi-Sync'
	G_CHECK_ROOT_USER
	G_CHECK_ROOTFS_RW
	G_INIT
	# Import DietPi-Globals --------------------------------------------------------------

	# Restart rsync service, if stopped during DietPi-Sync run: https://github.com/MichaIng/DietPi/issues/1869
	SERVICE_CONTROL=0
	G_EXIT_CUSTOM(){ (( $SERVICE_CONTROL )) && G_EXEC systemctl start rsync; }

	# Grab input (valid interger)
	disable_error=1 G_CHECK_VALIDINT "$1" && INPUT=$1 || INPUT=0

	#/////////////////////////////////////////////////////////////////////////////////////
	# Sync System
	#/////////////////////////////////////////////////////////////////////////////////////
	EXIT_CODE=0

	# File paths
	readonly FP_SETTINGS='/boot/dietpi/.dietpi-sync_settings'
	readonly FP_FILTER_INCLUDE_EXCLUDE='.dietpi-sync_include_exclude'
	readonly FP_USER_FILTER_INCLUDE_EXCLUDE='/boot/dietpi/.dietpi-sync_inc_exc'
	readonly FP_LOG='.dietpi-sync.log'
	readonly FP_LOG_ALT='/var/tmp/dietpi/logs/dietpi-sync.log'
	FP_SOURCE='/mnt/source'
	FP_TARGET='/mnt/target'

	# Extra settings
	SYNC_DELETE_MODE=0
	SYNC_CRONDAILY=0

	Create_Filter_Include_Exclude(){

		# Exclude files by name
		cat << _EOF_ > $FP_FILTER_INCLUDE_EXCLUDE
- $FP_LOG
- $FP_FILTER_INCLUDE_EXCLUDE
- .swap*
- *.tmp
# - MS Windows specific
- Thumbs.db
- desktop.ini
- SyncToy* # MS SyncToy
- System Volume Information # causes error code 23 (permission denied)
_EOF_

		# Exclude specific files/dirs by path
		local aexclude=(

			"$FP_TARGET"
			"$FP_SETTINGS"
			"$FP_USER_FILTER_INCLUDE_EXCLUDE"
			/var/swap

		)

		for i in "${aexclude[@]}"
		do

			# Exclude only, if inside source location, via path, relative to source location
			[[ $i == ${FP_SOURCE}/* ]] && echo "- $(realpath --relative-to="$FP_SOURCE" "$i")" >> $FP_FILTER_INCLUDE_EXCLUDE

		done

		unset aexclude

		# Add users additional list
		[[ -f $FP_USER_FILTER_INCLUDE_EXCLUDE ]] && cat $FP_USER_FILTER_INCLUDE_EXCLUDE >> $FP_FILTER_INCLUDE_EXCLUDE

	}

	Run_Sync(){

		# Stop rsync service: https://github.com/MichaIng/DietPi/issues/1869
		if systemctl is-active rsync &> /dev/null; then

			SERVICE_CONTROL=1
			G_EXEC systemctl stop rsync

		fi

		# Pre-create target dir, which also checks R/W access
                # - Remove previous mkdir log
		[[ -f $FP_LOG_ALT ]] && rm $FP_LOG_ALT
		mkdir -p "$FP_TARGET" &> $FP_LOG

		# Error: Dir not found
		if [[ ! -d $FP_TARGET ]]; then

			G_WHIP_MSG "[FAILED] Unable to pre-create target directory: $FP_TARGET\n\n\"mkdir\" reported the following error:\n$(<$FP_LOG)"
			# We cannot log to target dir, use $FP_LOG_ALT instead
			echo "$(date +"%Y-%m-%d_%T") [FAILED] Unable to pre-create target directory: $FP_TARGET" >> $FP_LOG
			mv $FP_LOG $FP_LOG_ALT

		# Error: Empty source dir
		elif [[ ! $(ls -A "$FP_SOURCE") ]]; then

			G_WHIP_MSG "[FAILED] The chosen source directory is empty: $FP_SOURCE\n\nPlease check it's path and in case verify that it's mount is active."
			# Log to target dir directly, preserve previous rsync log
			echo "$(date +"%Y-%m-%d_%T") [FAILED] The chosen source directory is empty: $FP_SOURCE" >> "$FP_TARGET/$FP_LOG"

		# Error: Rsync still running
		elif pgrep '[r]sync' &> /dev/null; then

			G_WHIP_MSG '[FAILED] Rsync is already running and failed to stop gracefully\n\nPlease check active rsync processes e.g. via "htop" and finish or kill them.'
			# Log to target dir directly, preserve previous rsync log
			echo "$(date +"%Y-%m-%d_%T") [FAILED] Rsync is already running and failed to stop gracefully" >> "$FP_TARGET/$FP_LOG"

		# Start sync, beginning with dry run
		else

			# Generate Exclude/Include lists
			Create_Filter_Include_Exclude

			# Rsync options
			local aoptions=('-aHP4' '--info=name0' '--info=progress2' "--exclude-from=$FP_FILTER_INCLUDE_EXCLUDE" "--log-file=$FP_LOG")
			# - Delete mode?
			(( $SYNC_DELETE_MODE )) && aoptions+=('--delete' '--delete-excluded')

			# Dry run
			G_DIETPI-NOTIFY 3 $G_PROGRAM_NAME "Dry run $FP_SOURCE > $FP_TARGET"
			rsync --dry-run --stats "${aoptions[@]}" "$FP_SOURCE"/ "$FP_TARGET"/ > .dietpi-sync_result
			EXIT_CODE=$?
			if (( ! $EXIT_CODE )); then

				# Free space check
				# - NB: Working in KiB until end MiB conversion, as, don't like using long long int, and, KiB should offer a good end result.
				local old_backup_size=$(du -ks "$FP_TARGET" | mawk '{print $1}') # Actual disk space usage
				local new_backup_size=$(( $(grep -m1 '^Total file size:' .dietpi-sync_result | sed 's/[^0-9]*//g') / 1024 + 1 )) # Theoretical, data size only
				# - NB: Number of files $4 includes files + dirs + links.
				#	Actually, we don't want to count links, as they don't use block sizes.
				#	But scraping files + dirs is too much effort, as the output format depends on their existence.
				local total_file_count=$(mawk '/^Number of files:/ {print $4;exit}' .dietpi-sync_result | sed 's/[^0-9]*//g')
				local target_fs_blocksize=$(stat -fc %s "$FP_TARGET")
				new_backup_size=$(( $new_backup_size + $total_file_count * $target_fs_blocksize / 1024 + 1 )) # Add one block size to each file as worst case result
				local end_result=$(( ( $new_backup_size - $old_backup_size ) / 1024 + 1 ))

				local menu_test="[  OK  ] Dry run completed (NO changes made):\n - $FP_SOURCE > $FP_TARGET"
				G_WHIP_DEFAULT_ITEM='Sync'
				if ! G_CHECK_FREESPACE "$FP_TARGET" $end_result; then

					if (( $INPUT == 1 )); then

						echo "$(date +"%Y-%m-%d_%T") [FAILED] Insufficient free space" >> "$FP_TARGET/$FP_LOG"
						return 1

					else

						menu_test="[WARNING] Insufficient free space\n - $FP_SOURCE > $FP_TARGET\n
The target location appears to have insufficient free space to successfully finish the sync. However, this check is a rough estimate in reasonable time and may be slightly incorrect.\n
Continue only if you know what you are doing and after checking the log!"
						G_WHIP_DEFAULT_ITEM='Cancel'

					fi

				fi

				while :
				do

					G_WHIP_MENU_ARRAY=(

						'Sync' ': Continue with real sync'
						'Log' ': View log file for details'
						'Cancel' ': Abort sync, no changes will be made'

					)

					G_WHIP_MENU "$menu_test\n\n$(sed -n 4,9p .dietpi-sync_result)"

					if [[ $G_WHIP_RETURNED_VALUE == 'Log' ]]; then

						G_WHIP_VIEWFILE $FP_LOG
						G_WHIP_DEFAULT_ITEM='Log'

					elif [[ $G_WHIP_RETURNED_VALUE == 'Sync' || $INPUT == 1 ]]; then

						break

					else

						return

					fi

				done

				# Sync
				G_DIETPI-NOTIFY 3 $G_PROGRAM_NAME "Sync $FP_SOURCE > $FP_TARGET"
				# - Clear log file from dry run
				> $FP_LOG
				rsync "${aoptions[@]}" "$FP_SOURCE"/ "$FP_TARGET"/
				EXIT_CODE=$?

			fi

			G_DIETPI-NOTIFY -1 $EXIT_CODE $G_PROGRAM_NAME

			if (( $EXIT_CODE )); then

				echo "$(date +"%Y-%m-%d_%T") [FAILED] Please see the log file for more information: $FP_TARGET/$FP_LOG" >> $FP_LOG
				G_WHIP_MSG "[FAILED] $FP_SOURCE > $FP_TARGET\n\nYou will given an option to view the logfile on the next screen. Please check it for information and/or errors."

			else

				echo "$(date +"%Y-%m-%d_%T") [  OK  ] Sync completed" >> $FP_LOG
				G_WHIP_MSG "[  OK  ] Sync completed:\n - $FP_SOURCE > $FP_TARGET"

			fi

			log=1 G_WHIP_VIEWFILE $FP_LOG
			mv $FP_LOG "$FP_TARGET"/

		fi

	}

	Check_Available_DietPi_Mounts(){

		local samba_mount_out=$(df -h | grep "$fp_samba_mount")
		if [[ $samba_mount_out ]]; then

			samba_mount_available=1
			samba_mount_text=$(mawk '{print "Size: "$2"iB | Available: "$4"iB"}' <<< "$samba_mount_out")

		fi

	}

	#/////////////////////////////////////////////////////////////////////////////////////
	# Settings File
	#/////////////////////////////////////////////////////////////////////////////////////
	Write_Settings_File(){

		cat << _EOF_ > $FP_SETTINGS
FP_SOURCE='$FP_SOURCE'
FP_TARGET='$FP_TARGET'
SYNC_DELETE_MODE=$SYNC_DELETE_MODE
SYNC_CRONDAILY=$SYNC_CRONDAILY
_EOF_

	}

	Read_Settings_File(){ [[ -f $FP_SETTINGS ]] && . $FP_SETTINGS; }

	#/////////////////////////////////////////////////////////////////////////////////////
	# MENUS
	#/////////////////////////////////////////////////////////////////////////////////////
	TARGETMENUID=0

	SYNC_MODE_TEXT=
	SYNC_CRONDAILY_TEXT=

	# TARGETMENUID=0
	MENU_LASTITEM_MAIN=
	Menu_Main(){

		(( $SYNC_DELETE_MODE )) && SYNC_MODE_TEXT='On' || SYNC_MODE_TEXT='Off'
		(( $SYNC_CRONDAILY )) && SYNC_CRONDAILY_TEXT='On' || SYNC_CRONDAILY_TEXT='Off'

		if [[ -f $FP_TARGET/$FP_LOG ]]; then

			local sync_last_status=$(tail -1 "$FP_TARGET/$FP_LOG")

		elif [[ -f $FP_LOG_ALT ]]; then

			local sync_last_status=$(tail -1 $FP_LOG_ALT)

		else

			local sync_last_status='No previous sync found in target directory.'

		fi

		G_WHIP_MENU_ARRAY=(

			'' '●─ Info '
			'Help' ": What does $G_PROGRAM_NAME do?"
			'' '●─ Options '
			'Source Location' ": [$FP_SOURCE]"
			'Target Location' ": [$FP_TARGET]"
			'Delete Mode' ": [$SYNC_MODE_TEXT]"
			'Daily Sync' ": [$SYNC_CRONDAILY_TEXT]"
			'' '●─ Run '
			'Dry run + Sync' ': Sync from Source to Target (Dry run included!)'

		)

		G_WHIP_DEFAULT_ITEM=$MENU_LASTITEM_MAIN
		G_WHIP_BUTTON_CANCEL_TEXT='Exit'
		if G_WHIP_MENU "Last sync status:\n  $sync_last_status"; then

			MENU_LASTITEM_MAIN=$G_WHIP_RETURNED_VALUE

			case "$G_WHIP_RETURNED_VALUE" in

				'Source Location') TARGETMENUID=2;;

				'Target Location') TARGETMENUID=1;;

				'Help')

					G_WHIP_MSG 'DietPi-Sync is a program that allows you to duplicate a directory from one location (Source) to another (Target).\n
For example: If we want to duplicate (sync) the data on our external USB HDD to another location, we simply select the USB HDD as the source, then, select a target location.
The target location can be anything from a networked samba fileserver, or even an FTP server.\n
Each sync includes a leading dry run, after which you can check the expected result before deciding if you want to continue with the actual sync.\n
More information:\n - https://dietpi.com/docs/dietpi_tools/#dietpi-sync'

				;;

				'Delete Mode') Menu_Set_Sync_Delete_Mode;;

				'Daily Sync') Menu_Set_CronDaily;;

				'Dry run + Sync') Run_Sync;;

			esac

		else

			Menu_Exit

		fi

	}

	Menu_Exit(){

		G_WHIP_SIZE_X_MAX=50
		G_WHIP_YESNO "Exit $G_PROGRAM_NAME?" && TARGETMENUID=-1

	}

	# # TARGETMENUID: 2=Source | 1=Target
	MENU_LASTITEM_SET_DIRS=
	Menu_Set_Directories(){

		local current_directory=$FP_TARGET
		local current_mode_text='Target'
		local whip_description_text="Please select the $current_mode_text location.\nA copy of all the files and folders in the Source location will be created here.\n\nCurrent $current_mode_text location:\n$current_directory"
		if (( $TARGETMENUID == 2 )); then

			current_directory=$FP_SOURCE
			current_mode_text='Source'
			whip_description_text="Please select the $current_mode_text location.\nA copy of all the files and folder in this Source location, will be created at the Target location.\n\nCurrent $current_mode_text location:\n$current_directory"

		fi

		# Check for samba mount
		local fp_samba_mount='/mnt/samba'
		local samba_mount_available=0
		local samba_mount_text="Not mounted ($fp_samba_mount). Select to setup."
		Check_Available_DietPi_Mounts

		G_WHIP_MENU_ARRAY=(

			'Manual' ": Manually type your $current_mode_text directory."
			'List' ': Select from a list of available mounts/drives'
			'Samba Client' ": [$samba_mount_text]"

		)

		G_WHIP_DEFAULT_ITEM=$MENU_LASTITEM_SET_DIRS
		G_WHIP_BUTTON_CANCEL_TEXT='Back'
		if G_WHIP_MENU "$whip_description_text"; then

			MENU_LASTITEM_SET_DIRS=$G_WHIP_RETURNED_VALUE

			case "$G_WHIP_RETURNED_VALUE" in

				'List')

					if /boot/dietpi/dietpi-drive_manager 1; then

						local return_value=$(</tmp/dietpi-drive_manager_selmnt)
						[[ $return_value == '/' ]] && return_value='/mnt'

						if (( $TARGETMENUID == 2 )); then

							FP_SOURCE=$return_value

						else

							FP_TARGET="$return_value/dietpi-sync"

						fi

						TARGETMENUID=0

					fi

				;;

				'Manual') Input_User_Directory;;

				'Samba Client')

					if (( $samba_mount_available )); then

						if (( $TARGETMENUID == 2 )); then

							FP_SOURCE=$fp_samba_mount

						else

							FP_TARGET="$fp_samba_mount/dietpi-sync"

						fi

						TARGETMENUID=0

					else

						Prompt_Setup_Samba_Mount

					fi

				;;

			esac

		else

			TARGETMENUID=0 # Main menu

		fi

	}

	Menu_Set_Sync_Delete_Mode(){

		G_WHIP_MENU_ARRAY=(

			'Off' ': Only add/update, but never remove data in target location'
			'On' ': Sync removals from source to target as well => Exact copy'

		)

		G_WHIP_DEFAULT_ITEM=$SYNC_MODE_TEXT
		if G_WHIP_MENU 'Please select the Sync delete mode.\n
Off: (safe)\nIf files and folders exist in the Target location, that are not in the Source, they will be left alone.\n
On:  (WARNING, if in doubt, DO NOT enable)\nAn exact copy of the Source location will be created at the Target location.
If files are in the Target location that do not exist in the Source, they will be DELETED.'; then

			[[ $G_WHIP_RETURNED_VALUE == 'On' ]] && SYNC_DELETE_MODE=1 || SYNC_DELETE_MODE=0

		fi

	}

	Menu_Set_CronDaily(){

		G_WHIP_MENU_ARRAY=(

			'Off' ': Manual sync only'
			'On' ': Automatically sync once a day'

		)

		G_WHIP_DEFAULT_ITEM=$SYNC_CRONDAILY_TEXT
		if G_WHIP_MENU 'Off:\nThe user must manually sync using dietpi-sync.\n
On:\nA cron job will automatically run dietpi-sync, once a day.\n
(NOTICE):\nWhen using this feature, please run a manual sync and check the dry run log to verify what will happen.'; then

			[[ $G_WHIP_RETURNED_VALUE == 'On' ]] && SYNC_CRONDAILY=1 || SYNC_CRONDAILY=0

		fi

	}

	Input_User_Directory(){

		# TARGETMENUID: 2=Source | 1=Target
		local mode='target'
		(( $TARGETMENUID == 2 )) && mode='source'
		local fp_mode="FP_${mode^^}"
		G_WHIP_DEFAULT_ITEM=${!fp_mode}
		local text="Please enter the full path to the desired $mode directory.\n\n - eg: /mnt/$mode"

		while :
		do

			if G_WHIP_INPUTBOX "$text"; then

				# Sanity checks
				# - Full path required
				if [[ $G_WHIP_RETURNED_VALUE != /* ]]; then

					text="ERROR: Please enter the $mode location as full path with leading slash: /\n\n - eg: /mnt/$mode"

				# - Do not allow to sync from or to root
				elif [[ $G_WHIP_RETURNED_VALUE == / ]]; then

					text="ERROR: You entered the root directory as $mode location: /\n\n - Please use \"dietpi-backup\" instead for system backups and recoveries."

				else

					declare -g $fp_mode="${G_WHIP_RETURNED_VALUE%/}"
					TARGETMENUID=0
					return

				fi

			else

				return

			fi

			G_WHIP_DEFAULT_ITEM=$G_WHIP_RETURNED_VALUE

		done

	}

	Prompt_Setup_Samba_Mount(){ G_WHIP_YESNO "$samba_mount_text\n\nWould you like to run DietPi-Drive_Manager and setup your Samba Client Mount now?" && /boot/dietpi/dietpi-drive_manager; }

	#/////////////////////////////////////////////////////////////////////////////////////
	# Main Loop
	#/////////////////////////////////////////////////////////////////////////////////////
	# Pre-reqs, install if required
	G_AG_CHECK_INSTALL_PREREQ rsync

	# Generate optional user include/exclude file
	if [[ ! -f $FP_USER_FILTER_INCLUDE_EXCLUDE ]]; then

		cat << _EOF_ > $FP_USER_FILTER_INCLUDE_EXCLUDE
#$G_PROGRAM_NAME | Custom include/exclude filters
#
#To EXCLUDE (-) all files by name:
#- file
#To EXCLUDE (-) a single file by path, relative to source directory:
#- /relative/path/to/file
#To EXCLUDE (-) a specific directory by path, relative to source directory:
#- /relative/path/to/directory/
#
#To INCLUDE (+) something from within an otherwise excluded directory:
#+ /relative/path/to/directory/include_this

_EOF_

	fi

	Read_Settings_File

	#-----------------------------------------------------------------------------
	# Run sync
	if (( $INPUT == 1 )); then

		Run_Sync

	#-----------------------------------------------------------------------------
	# Run menu
	elif (( $INPUT == 0 )); then

		until (( $TARGETMENUID < 0 ))
		do

			G_TERM_CLEAR

			if [[ $TARGETMENUID == [12] ]]; then

				Menu_Set_Directories

			else

				Menu_Main

			fi

		done

		Write_Settings_File

	fi

	#-----------------------------------------------------------------------------------
	exit $EXIT_CODE
	#-----------------------------------------------------------------------------------
}
